//=============================================================================
// CoolStorage.NET - .NET Object Relational Mapping Library 
//
// Copyright (c) 2004-2007 Philippe Leybaert
//
// Permission is hereby granted, free of charge, to any person obtaining a copy 
// of this software and associated documentation files (the "Software"), to deal 
// in the Software without restriction, including without limitation the rights 
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
// copies of the Software, and to permit persons to whom the Software is 
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in 
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
//=============================================================================

using System;
using System.Collections;
using System.Collections.Specialized;
using System.Data;
using System.Reflection;

namespace Activa.CoolStorage
{
	internal class CSSchema
	{
		private static readonly object    _staticLock = new object();

		private static readonly Hashtable _schemaMap = new Hashtable();
		private static readonly Hashtable _typeToTableMap = new Hashtable();
		private static readonly Hashtable _typeToContextMap = new Hashtable();

		private static bool      _firstSchemaCreated = false;

		private readonly CSSchemaColumnCollection _columnList    = new CSSchemaColumnCollection();
		private readonly CSSchemaFieldCollection  _fieldList     = new CSSchemaFieldCollection();
		private readonly CSSchemaColumnCollection _keyColumnList = new CSSchemaColumnCollection();
		private readonly StringCollection         _columnsToRead = new StringCollection();
		private CSSchemaColumn           _identityColumn;
		private PropertyInfo             _toStringProperty;
		private readonly Type                     _classType;
	    private readonly bool                     _nonAbstract = false;
		private string				 	 _defaultSortExpression;
		private string                   _tableName;
		private string                   _context;

		internal Type ClassType
		{
			get
			{
				return _classType;
			}
		}

		internal CSSchemaColumn IdentityColumn
		{
			get
			{
				return _identityColumn;
			}
		}

		internal PropertyInfo ToStringProperty
		{
			get
			{
				return _toStringProperty;
			}
		}

		internal CSSchemaColumnCollection KeyColumns
		{
			get
			{
				return _keyColumnList;
			}
		}

		internal CSSchemaColumnCollection Columns
		{
			get
			{
				return _columnList;
			}
		}

		internal CSSchemaFieldCollection Fields
		{
			get
			{
				return _fieldList;
			}
		}

		internal StringCollection ColumnsToRead
		{
			get
			{
				return _columnsToRead;
			}
		}

		internal string TableName
		{
			get
			{
				return _tableName;
			}
		}

		internal CSDataProvider DB
		{
			get
			{
				return CSConfig.GetDB(_context);
			}
		}

		internal string DefaultSortExpression
		{
			get { return _defaultSortExpression; }
		}

		internal static void ChangeMapTo(Type type , string tableName , string context)
		{
			lock (_staticLock)
			{
				if (_firstSchemaCreated)
					throw new CSException("MapTo() override not allowed after application start");

				if (context == null)
					context = "_DEFAULT_";

				_typeToTableMap[type] = tableName;
				_typeToContextMap[type] = context;
			}
		}

		internal CSSchema(Type objType)
		{
			lock (_staticLock)
				_firstSchemaCreated = true;

            if (objType.BaseType.IsGenericType && objType.BaseType.IsGenericType && objType.BaseType.BaseType.GetGenericTypeDefinition() == typeof(CSObject<>))
            {
                _classType = objType;
                _nonAbstract = true;
            }
            else if (objType.IsAbstract)
                _classType = objType;
            else
                _classType = objType.BaseType;

			CreateContext();

			CreateColumns();

			CreateFields();

			CreateColumnsToRead();
		}
	
		internal static CSSchema Get(Type objectType)
		{
			if (!objectType.IsSubclassOf(typeof(CSObject)))
				throw new CSException("CSSchema.Get() called with type not derived from CSObject");

			lock (_staticLock)
			{
				CSSchema schema = (CSSchema) _schemaMap[objectType];

				if (schema == null)
				{
					schema = new CSSchema(objectType);

					_schemaMap[objectType] = schema;

					schema.CreateRelations();
				}

				return schema;
			}
		}

		private void CreateContext()
		{
			lock (_staticLock)
			{
				if (_typeToTableMap.ContainsKey(_classType))
				{
					_tableName = (string) _typeToTableMap[_classType];
					_context   = (string) _typeToContextMap[_classType];
				}
				else
				{
					MapToAttribute[] mapToAttributes = (MapToAttribute[]) ClassType.GetCustomAttributes(typeof(MapToAttribute),true);
					DefaultSortExpressionAttribute[] sortAttributes = (DefaultSortExpressionAttribute[]) ClassType.GetCustomAttributes(typeof(DefaultSortExpressionAttribute), true);

					if (mapToAttributes.Length != 1)
						throw new CSException("No MapTo() attribute defined for class " + ClassType.Name);

					_tableName = mapToAttributes[0].Name;
					_context   = mapToAttributes[0].Context;

					if (sortAttributes.Length > 0)
						_defaultSortExpression = sortAttributes[0].Expression;
				}
			}

			if (_context == null)
				_context = "_DEFAULT_";
		}

		private void CreateColumns()
		{
			DataTable schemaTable = DB.GetSchemaTable(TableName);

			_identityColumn = null;
			
			_columnList.Clear();
			_keyColumnList.Clear();

			bool hasHidden = schemaTable.Columns.Contains("IsHidden");
			bool hasIdentity = schemaTable.Columns.Contains("IsIdentity");

			foreach (DataRow schemaRow in schemaTable.Rows)
			{
				CSSchemaColumn schemaColumn = new CSSchemaColumn();

				if (hasHidden && !schemaRow.IsNull("IsHidden") && (bool)schemaRow["IsHidden"])
					schemaColumn.Hidden = true;

				schemaColumn.IsKey     = (bool)   schemaRow["IsKey"];
				schemaColumn.AllowNull = (bool)   schemaRow["AllowDBNull"];
				schemaColumn.Name      = (string) schemaRow["ColumnName"];
				schemaColumn.ReadOnly  = (bool)   schemaRow["IsReadOnly"];
				schemaColumn.Identity  = (bool)   schemaRow["IsAutoIncrement"];
				schemaColumn.DataType  = (Type)   schemaRow["DataType"];
				schemaColumn.Size      = (int)    schemaRow["ColumnSize"];

				if (hasIdentity && !schemaRow.IsNull("IsIdentity") && (bool)schemaRow["IsIdentity"])
					schemaColumn.Identity = true;

				if (schemaColumn.IsKey)
					_keyColumnList.Add(schemaColumn);

				if (schemaColumn.Identity)
					_identityColumn = schemaColumn;

				_columnList.Add(schemaColumn);
			}
		}

		private void CreateFields()
		{
			PropertyInfo[] propInfoList = ClassType.GetProperties(BindingFlags.Instance|BindingFlags.Public|BindingFlags.NonPublic);

			bool foundDefaultSort = false;

			foreach (PropertyInfo propInfo in propInfoList)
			{
				if (propInfo.DeclaringType == typeof(CSObject))
					continue;

			    MethodInfo getMethod = propInfo.GetGetMethod();
                MethodInfo setMethod = propInfo.GetSetMethod();

                if ((getMethod == null && setMethod == null) || (getMethod != null && !getMethod.IsAbstract && !_nonAbstract))
                    continue;

				CSSchemaField schemaField = new CSSchemaField(propInfo,this);

				if (schemaField.MappedColumn != null || schemaField.Relation != null)
					_fieldList.Add(schemaField);

				if (propInfo.IsDefined(typeof(ToStringAttribute),true))
					_toStringProperty = propInfo;

				DefaultSortAttribute[] sortAttributes = (DefaultSortAttribute[])propInfo.GetCustomAttributes(typeof(DefaultSortAttribute), true);

				if (sortAttributes.Length > 0)
				{
					if (foundDefaultSort)
						throw new CSException(String.Format("Field [{0}.{1}] : only one field can have the DefaultSort attribute", ClassType.Name, propInfo.Name));

					if (_defaultSortExpression != null && _defaultSortExpression.Length > 0)
						throw new CSException(String.Format("Field [{0}.{1}] has DefaultSort attribute, but class already has a DefaultSortExpression attribute",ClassType.Name,propInfo.Name));

					_defaultSortExpression = schemaField.Name;

					if (sortAttributes[0].SortDirection == CSSort.Descending)
						_defaultSortExpression += "-";

					foundDefaultSort = true;
				}
			}

			foreach (CSSchemaColumn schemaColumn in _columnList)
			{
				if (schemaColumn.MappedField == null)
					_fieldList.Add(new CSSchemaField(schemaColumn,this));
			}
		}

		internal void CreateRelations()
		{
			foreach (CSSchemaField schemaField in _fieldList)
			{
				if (schemaField.Relation == null)
					continue;

				if (schemaField.Relation.Attribute.LocalKey != null)
					schemaField.Relation.LocalKey = schemaField.Relation.Attribute.LocalKey;

				if (schemaField.Relation.Attribute.ForeignKey != null)
					schemaField.Relation.ForeignKey = schemaField.Relation.Attribute.ForeignKey;

				if (schemaField.Relation.Attribute is OneToManyAttribute)
				{
					schemaField.Relation.RelationType = CSSchemaRelationType.OneToMany;

                    Type collectionType = schemaField.FieldType;

					while (!collectionType.IsGenericType || collectionType.GetGenericTypeDefinition() != typeof(CSList<>))
                        collectionType = collectionType.BaseType;

                    schemaField.Relation.ForeignType = collectionType.GetGenericArguments()[0];

					if (schemaField.Relation.LocalKey == null && KeyColumns.Count == 1)
						schemaField.Relation.LocalKey = KeyColumns[0].Name;

					if (schemaField.Relation.ForeignKey == null)
						schemaField.Relation.ForeignKey = schemaField.Relation.LocalKey;

					if (schemaField.Relation.LocalKey == null)
						throw new CSException("OneToMany relation [" + schemaField.Name + "] for class [" + ClassType.Name + "] cannot be created. Local key is not supplied and no single primary key exists");

					if (_columnList[schemaField.Relation.LocalKey] == null)
						throw new CSException("OneToMany relation [" + schemaField.Name + "] for class [" + ClassType.Name + "] cannot be created. Local key [" + schemaField.Relation.LocalKey + "] not defined in DB");
				}

				if (schemaField.Relation.Attribute is ManyToOneAttribute)
				{
					schemaField.Relation.RelationType = CSSchemaRelationType.ManyToOne;

					schemaField.Relation.ForeignType = schemaField.FieldType;

					if (schemaField.Relation.ForeignKey == null && Get(schemaField.FieldType).KeyColumns.Count == 1)
						schemaField.Relation.ForeignKey = Get(schemaField.FieldType).KeyColumns[0].Name;

					if (schemaField.Relation.LocalKey == null)
						schemaField.Relation.LocalKey = schemaField.Relation.ForeignKey;

					if (schemaField.Relation.ForeignKey == null)
						throw new CSException("ManyToOne relation [" + schemaField.Name + "] for class [" + ClassType.Name + "] cannot be created. Foreign key is not supplied and related table has no single primary");

					if (_columnList[schemaField.Relation.LocalKey] == null)
						throw new CSException("ManyToOne relation [" + schemaField.Name + "] for class [" + ClassType.Name + "] cannot be created. Local key [" + schemaField.Relation.LocalKey + "] not defined in DB");
				}

				if (schemaField.Relation.Attribute is OneToOneAttribute)
				{
					schemaField.Relation.RelationType = CSSchemaRelationType.OneToOne;

					schemaField.Relation.ForeignType = schemaField.FieldType;

					if (schemaField.Relation.LocalKey == null && schemaField.Relation.ForeignKey == null)
						throw new CSException("LocalKey or ForeignKey is required for OneToOne relation <" + schemaField.Name + "> in class <" + ClassType.Name + ">");

					if (schemaField.Relation.LocalKey == null)
						schemaField.Relation.LocalKey = schemaField.Relation.ForeignKey;

					if (schemaField.Relation.ForeignKey == null)
						schemaField.Relation.ForeignKey = schemaField.Relation.LocalKey;

					if (_columnList[schemaField.Relation.LocalKey] == null)
						throw new CSException("OneToOne relation <" + schemaField.Name + "> for class <" + ClassType.Name + "> cannot be created. Local key <" + schemaField.Relation.LocalKey + "> not defined in DB");
				}

				if (schemaField.Relation.Attribute is ManyToManyAttribute)
				{
					schemaField.Relation.RelationType = CSSchemaRelationType.ManyToMany;

					Type collectionType = schemaField.FieldType;

					while (!collectionType.IsGenericType || collectionType.GetGenericTypeDefinition() != typeof(CSList<>))
						collectionType = collectionType.BaseType;

					schemaField.Relation.ForeignType = collectionType.GetGenericArguments()[0];

					schemaField.Relation.LinkTable      = ((ManyToManyAttribute) schemaField.Relation.Attribute).LinkTable;
					schemaField.Relation.ForeignLinkKey = ((ManyToManyAttribute) schemaField.Relation.Attribute).ForeignLinkKey;
					schemaField.Relation.LocalLinkKey   = ((ManyToManyAttribute) schemaField.Relation.Attribute).LocalLinkKey;
					schemaField.Relation.PureManyToMany = ((ManyToManyAttribute) schemaField.Relation.Attribute).Pure;

					if (schemaField.Relation.LocalKey == null)
						schemaField.Relation.LocalKey = KeyColumns[0].Name;

					if (schemaField.Relation.ForeignKey == null)
						schemaField.Relation.ForeignKey = schemaField.Relation.ForeignSchema.KeyColumns[0].Name;

					if (schemaField.Relation.LocalLinkKey == null)
						schemaField.Relation.LocalLinkKey = schemaField.Relation.LocalKey;

					if (schemaField.Relation.ForeignLinkKey == null)
						schemaField.Relation.ForeignLinkKey = schemaField.Relation.ForeignKey;


					if (_columnList[schemaField.Relation.LocalKey] == null)
						throw new CSException("ManyToMany relation [" + schemaField.Name + "] for class [" + ClassType.Name + "] cannot be created. Local key [" + schemaField.Relation.LocalKey + "] not defined in DB");
				}
			}
		}

		private void CreateColumnsToRead()
		{
			_columnsToRead.Clear();

			foreach (CSSchemaColumn schemaColumn in Columns)
			{
				if (schemaColumn.MappedField != null && schemaColumn.MappedField.Lazy)
					continue;

				_columnsToRead.Add(schemaColumn.Name);
			}

			if (_columnsToRead.Count < 1)
				throw new CSException("No data fields mapped or primary key is lazy for object type <" + ClassType.Name + ">");
		}
	}
}
